﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;

namespace Xml2Yaml
{
    /// <summary>
    /// http://yaml.org/xml.html
    /// Mappings are expressed as elements, where the mapping key is represented using the element's name and the mapping value as the element's content. This restriction implies that a given tag name may not occur twice within a given context and that the order of the tag names is not significant.
    /// Lists are expressed as an orderd sequence of elements having a special name, the underscore. In this case, and only in this case is the ordering of elements significant and duplicate element names (just the underscore) are permitted. With these two rules, the bulk of YAML can be serialized.
    /// Scalars are modeled as a single text node child of a given element. Thus, mixed-content is not allowable.
    /// Unlike YAML, XML requires a root element with a name. Since this isn't part of the YAML information model, the element name can be arbitrarly chosen and should be discarded by the YAX->YAML conversion utility, the default element name can be 'yaml' to make things clear when going from YAML to XML.
    /// In this binding, an anchor on a node is modeled via the "anchor" attribute. Then, subsequent occurances of this node can be referenced by an empty element with an "alias" attribute having value matching a previous anchor.
    /// Since YAML tags are URIs, they can be used as XML namespaces, although since only a limited 'taguri' is allowed in YAML, using a XML namespace as a YAML tag is not generally possible.
    /// Nested keys, typed keys, or keys not matching the XML name production are emulated through a pair of elements, one for the key, the other for the value following each other sequentially with names "_key" and "_value". All XML names beginning with underscore are reserved so that this hack is clearly a hack.
    /// YAML comments are modeled directly as XML comments, only that they cannot be used to break up a text node or occur in any order differently than an associated YAML file.
    /// Escaping via XML character entities is allowed, but all other forms of entities are not.
    /// XML attributes lacking a namespace considered as elements, thus <a b="c"/> is considered as <a><b>c</b></a>
    /// All other features of XML are forbidden. Intermediate whitespace between elements used for readability is ignored.
    /// </summary>
    public class YamlWriter : StreamWriter
    {
        #region Memory variabls

        private const string YmalNamespace = @"http://yaml.org/xml";
        private const string SequenceNode = "_";
        private const string MutileLinePrefix = "|";

        private StringBuilder Buffer = new StringBuilder();
        private StringBuilder IndentationSpaces = new StringBuilder();

        private YamlConfig Configuration = null;
        private XPathParser XParser = null;

        public bool Initialized
        {
            private set;
            get;
        }

        #endregion

        public YamlWriter(Stream stream, string configurationFileName = "")
            : base(stream)
        {
            Initialized = Initialize(configurationFileName);
        }

        public YamlWriter(string outputPath, string configurationFileName = "")
            : base(outputPath)
        {
            Initialized = Initialize(configurationFileName);
        }

        public bool Convert(string inputFileName)
        {
            if (!File.Exists(inputFileName))
            {
                throw new System.IO.FileNotFoundException("Error: can not find file:" + inputFileName);
            }
            else
            {
                // using (StreamReader sr = new StreamReader(new FileStream(inputFileName, FileMode.Open, FileAccess.Read)))
                using (Stream sr = File.OpenRead(inputFileName))
                {
                    return Convert(sr);
                }
            }
        }

        public bool Convert(Stream sr)
        {
            using (ExpatWraper reader = new ExpatWraper(this))
            {
                reader.Initialize(null);
                return reader.Parse(sr);
            }
        }

        private bool Initialize(string configurationFileName)
        {
            try
            {
                Configuration = new YamlConfig(configurationFileName);
                if (Configuration == null || !Configuration.Initialized)
                {
                    Console.Error.WriteLine("Configuration file invalid!");
                }
                XParser = new XPathParser(Configuration.Namespaces);
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex.Message);
                return false;
            }
            return true;
        }

        /// <summary>
        /// Write Start Document
        /// </summary>
        internal void StartDocument()
        {
            const string header = @"--- %YAML:";
            const string tag = @"%TAG";

            WriteLine("# created by YamlWriter 1.0");
            Write(header + Configuration.Version);

            foreach (var item in Configuration.Tags)
            {
                WriteLine();
                Write(tag + " !" + item.Value + "! " + item.Key);
            }


        }

        /// <summary>
        /// Write End Document
        /// </summary>
        internal void EndDocument()
        {
            const string footer = "...";
            WriteLine();
            WriteLine(footer);
            Close();
        }

        /// <summary>
        /// Write Content
        /// </summary>
        /// <param name="text"></param>
        internal void WriteContent(string text)
        {
            if (Buffer.Length == 0)
            {
                text = YamlUtil.Trim(text);
            }
            if (!string.IsNullOrEmpty(text))
            {
                Buffer.Append(text);
            }
        }

        /// <summary>
        /// Write End Comment
        /// </summary>
        /// <param name="text"></param>
        internal void WriteComment(string text)
        {
            if (Configuration.DisplayComment)
            {
                WriteLine();
                WriteIndentWhiteSpace();
                Write("# " + YamlUtil.Escaped(text));
            }
        }

        /// <summary>
        /// Write End Element
        /// </summary>
        internal void WriteEndElement(string ns, string localName)
        {
            if (Configuration.IsIgnoreXPath(XParser.Attach(Buffer)))
            {
                Buffer.Clear();
            }
            else if (Buffer.Length > 0)
            {
                if (!string.IsNullOrEmpty(ns))
                {
                    ns = Configuration.GetNamespaceMapping(ns, false);
                }

                string xpath = string.Format(CultureInfo.InvariantCulture,
                    "//{0}{1}[.='{2}']",
                    string.IsNullOrEmpty(ns) ? string.Empty : (ns + ":"),
                    localName,
                    YamlUtil.EncodeXml(Buffer.ToString()));

                string text = Configuration.GetMappingFromXPath(XParser, xpath);
                if (text != xpath)
                {
                    Buffer.Clear();
                    Buffer.Append(text);
                }
            }

            if (Buffer.Length > 0)
            {
                string[] array = Buffer.ToString().TrimEnd().Split('\n');

                //handle mutl line
                if (array.Length > 1)
                {
                    WriteLine(MutileLinePrefix);
                    foreach (string s in array)
                    {
                        WriteIndentWhiteSpace();
                        WriteLine(s);
                    }
                }
                else
                {
                    Write(Buffer);
                }

                Buffer.Clear();
            }

            DecreaseSpace();

            XParser.Pop();
        }

        /// <summary>
        /// Write Start Element
        /// </summary>
        /// <param name="ns"></param>
        /// <param name="localName"></param>
        /// <param name="value"></param>
        /// <param name="attributes"></param>
        internal bool WriteStartElement(
                string ns,
                string localName,
                string value,
                IDictionary<AttributeType, string> attributes = null)
        {
            XParser.Push(ns, localName, value, attributes);

            if (Configuration.IsIgnoreXPath(XParser))
            {
                return false;
            }
            else
            {
                localName = Configuration.GetMappingFromXPath(XParser, localName);
            }

            WriteLine();
            WriteIndentWhiteSpace();

            if (!string.IsNullOrEmpty(ns))
            {
                ns = Configuration.GetNamespaceMapping(ns);
            }

            if (IsSequenceNode(localName))
            {
                Write(SequenceNode);
            }
            else
            {
                bool isSingleAornments = IsSingleAornments(attributes);
                if (string.IsNullOrEmpty(value))
                {
                    Write(
                        string.Format(CultureInfo.InvariantCulture, "{0}{1}{2}",
                                    ns,
                                    localName,
                                    isSingleAornments ? string.Empty : ": "));
                }
                else
                {
                    Write(
                        string.Format(CultureInfo.InvariantCulture,
                                "{0}{1}: {2}",
                                ns,
                                localName,
                                YamlUtil.Escaped(value)));
                }

                if (attributes != null)
                {
                    IncreaseSpace();

                    #region Write Attribue

                    foreach (var pair in attributes)
                    {
                        var attr = pair.Key;
                        string v = pair.Value;

                        if (!Configuration.IsIgnoreXPath(XParser.Attach(attr.LocalName)))
                        {
                            attr.LocalName = Configuration.GetMappingFromXPath(
                                        XParser,
                                        attr.LocalName
                                        );

                            bool b = HandleAdornments(attr, true);

                            if (b || string.IsNullOrEmpty(attr.LocalName))
                            {
                                Write(string.Format(CultureInfo.InvariantCulture,
                                    "{0}{1}", attr.LocalName, v));
                            }
                            else
                            {
                                WriteLine();
                                WriteIndentWhiteSpace();
                                Write(string.Format(CultureInfo.InvariantCulture,
                                    "{0}: {1}", attr.LocalName, v));
                            }
                        }
                    }


                    #endregion

                    DecreaseSpace();
                }
            }

            IncreaseSpace();

            return true;
        }

        #region Inner Default Handler

        private bool IsSequenceNode(string name)
        {
            return name == SequenceNode;
        }

        private bool IsSingleAornments(IDictionary<AttributeType, string> attributes)
        {
            if (attributes != null && attributes.Count == 1)
            {
                return HandleAdornments(attributes.First().Key, false);
            }

            return false;
        }

        private bool HandleAdornments(AttributeType attr, bool replace)
        {
            if (attr.NS == YmalNamespace) // default Namespace
            {
                switch (attr.LocalName)
                {
                    case "anchor":
                        if (replace)
                        {
                            attr.LocalName = ": &";
                        }
                        return true;
                    case "alias":
                        if (replace)
                        {
                            attr.LocalName = ": *";
                        }
                        return true;
                    default:
                        break;
                }
            }
            return false;
        }

        #endregion

        #region Indent Space

        private void WriteIndentWhiteSpace()
        {
            Write(IndentationSpaces);
        }

        private void IncreaseSpace()
        {
            IndentationSpaces.Append("  ");
        }

        private void DecreaseSpace()
        {
            if (IndentationSpaces.Length > 2)
            {
                IndentationSpaces.Remove(IndentationSpaces.Length - 2, 2);
            }
        }

        #endregion

    }
}
